/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.rest.data.service;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.ws.rs.Consumes;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.data.context.GetRequestContext;
import org.unidata.mdm.data.context.MergeRequestContext;
import org.unidata.mdm.data.context.MergeRequestContext.MergeRequestContextBuilder;
import org.unidata.mdm.data.context.PreviewRequestContext;
import org.unidata.mdm.data.context.PreviewRequestContext.PreviewRequestContextBuilder;
import org.unidata.mdm.data.dto.GetRecordDTO;
import org.unidata.mdm.data.dto.MergeRecordsDTO;
import org.unidata.mdm.data.service.DataRecordsService;
import org.unidata.mdm.rest.data.converter.ErrorInfoToRestErrorInfoConverter;
import org.unidata.mdm.rest.data.converter.EtalonPreviewConverter;
import org.unidata.mdm.rest.data.ro.EtalonRecordRO;
import org.unidata.mdm.rest.data.ro.ExtendedRecordRO;
import org.unidata.mdm.rest.data.ro.FullRecordRO;
import org.unidata.mdm.rest.data.ro.MergePreviewResponseRO;
import org.unidata.mdm.rest.data.ro.MergeRequestRO;
import org.unidata.mdm.rest.data.ro.MergedResponseRO;
import org.unidata.mdm.rest.data.ro.WinnerRO;
import org.unidata.mdm.rest.data.type.rendering.DataRestInputRenderingAction;
import org.unidata.mdm.rest.data.type.rendering.DataRestOutputRenderingAction;
import org.unidata.mdm.rest.system.ro.ErrorInfo;
import org.unidata.mdm.rest.system.ro.ErrorResponse;
import org.unidata.mdm.rest.system.ro.RestResponse;
import org.unidata.mdm.rest.system.service.AbstractRestService;
import org.unidata.mdm.system.service.RenderingService;

/**
 * The Class DataMergeRestService.
 */
@Path("/merge")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public class DataMergeRestService extends AbstractRestService {

    /** The data record service. */
    @Autowired
    private DataRecordsService dataRecordService;

    /** The data entity rest service. */
    @Autowired
    private DataEntityRestService dataEntityRestService;

    @Autowired
    private RenderingService renderingService;

    /**
     * Merge.
     *
     * @param mergeRequest
     *            the merge request
     * @return the response
     */
    @POST
    @Path("/apply")
    @Operation(
            summary = "Объединить дубликаты.",
            description = "Объединить несколько записей дубликатов в пользу одной записи.",
            method = HttpMethod.POST,
            requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = MergeRequestRO.class))),
            responses = {
                    @ApiResponse(responseCode = "200", description = "Request processed", content = @Content(schema = @Schema(implementation = RestResponse.class))),
                    @ApiResponse(responseCode = "500", description = "Error occurred", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
            }
    )
    public Response merge(MergeRequestRO mergeRequest) {

        if (Objects.isNull(mergeRequest) || mergeRequest.getEtalonIds().isEmpty()) {
            return notFound();
        }

        if (Objects.isNull(mergeRequest.getWinnerEtalonId())) {
            return notFound();
        }

        // UI historically send ids twice
        mergeRequest.getEtalonIds().removeIf(k -> mergeRequest.getWinnerEtalonId().equals(k));

        Map<String, String> attrs = toAttrMap(mergeRequest);
        String winnerEtalonId = mergeRequest.getWinnerEtalonId();
        List<GetRequestContext> contexts
                = mergeRequest
                    .getEtalonIds()
                    .stream()
                    .map(id -> GetRequestContext.builder()
                                .etalonKey(id)
                                .manualMergeAttrs(attrs)
                                .build())
                                .collect(Collectors.toList());

        ExtendedRecordRO newRecord = calculateMergedEtalon(mergeRequest);
        EtalonRecordRO record = (EtalonRecordRO) newRecord.getRecord();
        record.setEntityName(mergeRequest.getEntityName());
        record.setEtalonId(winnerEtalonId);

        MergeRequestContextBuilder ctxb
            = MergeRequestContext.builder()
                .etalonKey(winnerEtalonId)
                .duplicates(contexts)
                .manual(true);

        renderingService.renderInput(DataRestInputRenderingAction.MERGE_INPUT, ctxb, mergeRequest);

        MergeRecordsDTO result = dataRecordService.merge(ctxb.build());

        if (MapUtils.isNotEmpty(attrs)) {
            FullRecordRO wrapper = new FullRecordRO();
            wrapper.setDataRecord(record);
            dataEntityRestService.atomicUpsertRecord(wrapper);
        }

        List<ErrorInfo> errors = null;
        if (CollectionUtils.isNotEmpty(result.getErrors())) {
            errors = result
                    .getErrors()
                    .stream()
                    .map(ErrorInfoToRestErrorInfoConverter::convert)
                    .collect(Collectors.toList());
        }

        MergedResponseRO response = new MergedResponseRO();
        response.setEtalonId(winnerEtalonId);

        renderingService.renderOutput(DataRestOutputRenderingAction.MERGE_OUTPUT, result, response);

        return ok(new RestResponse<>(response, errors));

    }

    /**
     * Merge preview.
     *
     * @param mergeRequest
     *            the merge request
     * @return the response
     */
    @POST
    @Path("/preview")
    @Operation(
            summary = "Виртуально объединить дубликаты.",
            description = "Виртуально объединяет эталоны и возвращает материализованное представление объединенных эталонов.",
            method = HttpMethod.POST,
            requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = MergeRequestRO.class))),
            responses = {
                    @ApiResponse(responseCode = "200", description = "Request processed", content = @Content(schema = @Schema(implementation = MergePreviewResponseRO.class))),
                    @ApiResponse(responseCode = "500", description = "Error occurred", content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
            }
    )
    public Response mergePreview(MergeRequestRO mergeRequest) {

        if (Objects.isNull(mergeRequest) || mergeRequest.getEtalonIds().isEmpty()) {
            return notFound();
        }

        if (mergeRequest.getWinnerEtalonId() == null) {
            mergeRequest.setWinnerEtalonId(mergeRequest.getEtalonIds().remove(mergeRequest.getEtalonIds().size() - 1));
        }

        MergePreviewResponseRO response = new MergePreviewResponseRO();

        ExtendedRecordRO newRecord = calculateMergedEtalon(mergeRequest);
        response.setManualWinners(mergeRequest.getWinners());
        response.setManualDataRecord((EtalonRecordRO) newRecord.getRecord());
        mergeRequest.setWinners(null);

        ExtendedRecordRO oldRecord = calculateMergedEtalon(mergeRequest);
        response.setDataRecord((EtalonRecordRO) oldRecord.getRecord());
        response.setWinnerEtalonId(oldRecord.getWinnerEtalonId());

        autoWinners(oldRecord, response);
        return ok(response);
    }

    /**
     * Auto winners.
     *
     * @param newRecord
     *            the new record
     * @param response
     *            the response
     */
    private void autoWinners(ExtendedRecordRO newRecord, MergePreviewResponseRO response) {
        Map<String, String> autoWinners = newRecord.getAttributeWinnersMap();
        if(autoWinners == null) {
            return;
        }
        Map<String, WinnerRO> auto = new HashMap<>();
        autoWinners.forEach((k, v) -> {
            if (!auto.containsKey(v)) {
                WinnerRO winner = new WinnerRO();
                winner.setEtalonId(v);
                auto.put(v, winner);
            }
            auto.get(v).getPaths().add(k);
        });
        response.setWinners(new ArrayList<>(auto.values()));
    }

    /**
     * Calculate merged etalon.
     *
     * @param mergeRequest
     *            the merge request
     * @return the extended record RO
     */
    private ExtendedRecordRO calculateMergedEtalon(MergeRequestRO mergeRequest) {

        Map<String, String> attrs = toAttrMap(mergeRequest);
        PreviewRequestContextBuilder ctxb = PreviewRequestContext.builder()
                .attributes(attrs)
                .etalonKey(mergeRequest.getWinnerEtalonId() == null
                         ? mergeRequest.getEtalonIds().remove(mergeRequest.getEtalonIds().size() - 1)
                         : mergeRequest.getWinnerEtalonId())
                .forDate(new Date())
                .forLastUpdate(null)
                .duplicates(mergeRequest.getEtalonIds().stream()
                        .map(etalonId -> GetRequestContext.builder().etalonKey(etalonId).build())
                        .collect(Collectors.toList()));

        renderingService.renderInput(DataRestInputRenderingAction.PREVIEW_INPUT, ctxb, mergeRequest);

        PreviewRequestContext ctx = ctxb.build();
        GetRecordDTO getRecordDTO = dataRecordService.preview(ctx);

        ExtendedRecordRO extendedRecordRO = EtalonPreviewConverter.convert(getRecordDTO);
        String winnerId = attrs.isEmpty()
                ? extendedRecordRO.getAttributeWinnersMap().entrySet().stream().findFirst().map(Entry::getValue).orElse(null)
                : ctx.getEtalonKey();

        extendedRecordRO.setWinnerEtalonId(winnerId);

        renderingService.renderOutput(DataRestOutputRenderingAction.PREVIEW_OUTPUT, getRecordDTO, extendedRecordRO);
        return extendedRecordRO;
    }

    /**
     * To attr map.
     *
     * @param mergeRequest
     *            the merge request
     * @return the map
     */
    private Map<String, String> toAttrMap(MergeRequestRO mergeRequest) {
        Map<String, String> attrs = new HashMap<>();
        mergeRequest.getWinners().forEach(el -> el
                .getPaths()
                .forEach(p -> attrs.put(p, el.getEtalonId())));
        return attrs;
    }
}
